<?php


$this->useClass('RL_Protocol');
$this->useLib('http');

/**
 * Класс RL_Protocol_Http -- HTTP протокол, который умеет посылать RL_Response.
 *
 * @package RL
 * @author  Lucky <lucky@jetstyle.ru>
 * @version $Id: Http.php 184 2007-10-09 14:58:29Z lucky $
 */
class RL_Protocol_Http extends RL_Protocol
{
	//~ ENUM (MODE_ATTACHMENT | MODE_DIRECT | MODE_AUTO)
	/**
	 * Режим отравки контента в виде вложенного файла.
	 *
	 * В браузере пользователя появится диалог "Сохранить как..."
	 */
	const MODE_ATTACHMENT = 1;
	/**
	 * Режим отравки контента в браузер.
	 *
	 * Контент окажется в окне браузера
	 */
	const MODE_DIRECT     = 0;
	/**
	 * Режим отравки контента должен выбираться автоматически.
	 */
	const MODE_AUTO     = NULL;

	static $status_codes = array(
		RL_OK       => 200,
		RL_NOTFOUND => 404,
	);

	/**
	 * download_mode режим отдачи контента -- в браузер или аттачем.
	 *
	 * FIXME : 2007-08-06 : lucky
	 * не нравится мне слово download в названии
	 *
	 * @var mixed
	 * @access public
	 */
	public $download_mode = self::MODE_AUTO;

	/**
	 * bufsize размер блоков, которыми отправлять данные из контента.
	 *
	 * @var int
	 * @access public
	 */
	public $bufsize = 16384;

	public $headers = array();

	//!a manipulator
	public function send (RL_Response $response)
	/**
	 * send отправить ответ.
	 *
	 * ООчень большая и страшная фукнция, которая по типу контента пытается-таки сформировать
	 * необходимые HTTP заголовки.
	 *
	 * @param RL_Response $response ответ
	 * @access public
	 * @return string -- по соглашению должен вернуть строку, но наверняка вернет пустую.
	 */
	{
		$rh      = $this->ctx->rh;
		$content = $response->getContent();
		$status  = $response->getStatus();
		$meta    = $content->getMeta();
		$SERVER  =& $rh->SERVER;
		$allow_send_content = TRUE;

		// побежали
		do {
			$from = NULL; $to = NULL;
			$this->setStatusCode($this->findStatusCode($status));

			// а нужно-ли отправлять контент?

			// lucky: http://ru.php.net/manual/ru/function.setcookie.php#48838
			//$this->setHeader('P3P', 'CP="NOI ADM DEV PSAi COM NAV OUR OTRo STP IND DEM"');

			// HACK : 2007-11-12 : lucky
			// для файлов
			// переписываем загловки, которые иногда кто-то Maybe {apache; php; proxy}
			// вставляет автоматически.
			// нужно с этим разбираться отдельно
			if ($content instanceof RL_View_File) {
				$this->setHeader('Cache-Control', '');
				$this->setHeader('Pragma', '');
				$this->setHeader('Expires', '');
			} else {
				$this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
				$this->setHeader('Pragma', 'no-cache');
				$this->setHeader('Expires', gmdate('D, d M Y H:i:s T', 1));
			}

			// если HTTP_IF_NONE_MATCH и контент актуален, посылаем лишь загловки
			if ($status == RL_OK && isset($SERVER['HTTP_IF_NONE_MATCH'])) {
				if ($meta && $SERVER['HTTP_IF_NONE_MATCH'] == $meta->getETag())
				{
					$this->setStatusCode(304); // Not Modified
					$allow_send_content = FALSE;

					if ($v = $meta->getLastModified()) {
						$this->setHeader('Last-Modified', gmdate( "D, d M Y H:i:s T", $v));
					}

					if ($v = $meta->getMaxAge()) {
						$this->setHeader('Expires',       gmdate( "D, d M Y H:i:s T", time() + $v));
						$this->setHeader('Cache-Control', "max-age={$v}, must-revalidate");
					}

					if ($v = $meta->getETag()) {
						$this->setHeader('ETag', $v);
					}
					break; // EXIT from do-while(false)
				}
			}

			// если HTTP_IF_MODIFIED_SINCE и контент актуален, посылаем лишь загловки
			if ($status === RL_OK && isset($SERVER['HTTP_IF_MODIFIED_SINCE'])) {
				// parse header
				$if_modified_since = preg_replace('/;.*$/', '', $SERVER['HTTP_IF_MODIFIED_SINCE']);

				if ($meta && $if_modified_since == $meta->getLastModified()) {
					// the browser's cache is still up to date
					$this->setStatusCode(304); // Not Modified
					$allow_send_content = FALSE;

					if ($v = $meta->getLastModified()) {
						$this->setHeader('Last-Modified', gmdate( "d, d m y h:i:s T", $v));
					}

					if ($v = $meta->getMaxAge()) {
						$this->setHeader('Expires',       gmdate( "D, d M Y H:i:s T", time() + $v));
						$this->setHeader('Cache-Control', "max-age={$v}, must-revalidate");
					}

					if ($v = $meta->getETag()) {
						$this->setHeader('ETag', $v);
					}
					break; // EXIT from do-while(false)
				}
			}

			// все таки, нужно отправить контент
			if ($meta) {
				if ($v = $meta->getLastModified()) {
					$this->setHeader('Last-Modified', gmdate( "D, d M Y H:i:s T", $v));
				}

				if ($v = $meta->getMaxAge()) {
					$this->setHeader('Expires',       gmdate( "D, d M Y H:i:s T", time() + $v));
					$this->setHeader('Cache-Control', "max-age={$v}, must-revalidate");
				}

				if ($v = $meta->getETag()) {
					$this->setHeader('ETag', $v);
				}

				if ($v = $meta->getContentType()) {
					$this->setHeader('Content-Type', $v);
				} elseif ($v = $meta->getMimeType()) {
					$this->setHeader('Content-Type', $v);
				}
				// определим $fmode : как будем отдавать контент -- напрямую или вложением ?
				$fmode = $this->getDownloadMode();
				if ($fmode === self::MODE_AUTO) {
					// определим $fmode сами
					switch ($meta->getMimeType()) {
						// это в браузер
					case 'application/x-javascript':
					case 'application/json':
					case 'text/javascript':
					case 'text/html':
					case 'text/xml':
					case 'text/plain':
					case 'text/css':
					case 'image/gif':
					case 'image/png':
					case 'image/jpeg':
						$fmode = self::MODE_DIRECT;
						break;
					default:
						// все остальное -- аттачами
						$fmode = self::MODE_ATTACHMENT;
					}
				}

				if ($fmode === self::MODE_ATTACHMENT) {
					$fname = $meta->getFileName();
					$this->setHeader('Content-Transfer-Encoding', 'binary');
					$this->setHeader('Content-Disposition', 'attachment; filename="'.$fname.'"');
				}

				// определим размер контента.
				// если запросили докачку, попробуем организовать

				if ($fsize = $meta->getLength()) {

					$from = 0;
					$to = $fsize;

					// качаем не все ?
					if (isset($SERVER['HTTP_RANGE'])) { //Partial download
						if (preg_match('#^bytes=(\\d+)-(\\d*)$#', $SERVER['HTTP_RANGE'], $matches)) { //parsing Range header
							$from = $matches[1];
							$to = $matches[2];

							if (empty($to)) {
								$to = $fsize - 1;  // -1  because end byte is included
								//(From HTTP protocol:
								// 'The last-byte-pos value gives the byte-offset of the last byte in the range;
								// that is, the byte positions specified are inclusive')
							}

							if ($from >= $fsize) {
								// клиент запросил больше, чем занимает файл.
								// по видимому, он уже целиком лежит у клиента.
								// контент не отправляем.
								$this->setStatusCode(416);
								$allow_send_content = FALSE;
							}
							$content_size = $to - $from + 1;

						} else {
							$this->setStatusCode(500); // HTTP/1.1 500 Internal Server Error
							$allow_send_content = FALSE; // FIXME : 2007-08-06 : lucky
							// возможно тут надо показать красивое сообщение об ошибке,
							// но кто этим займется?
							// Скорее всего, этим должен заниматься RH, проанализировав код статуса
						}
					} else {
						// качаем все
						$content_size = $fsize;
					}

					$this->setHeader('Content-Length', $content_size);

					if ($from !== 0 || $to !== $fsize) {
						$this->setHeader('Content-Range', $from.'-'.$to.'/'.$fsize);
					}
				}
			}

			// FIXME : 2007-08-06 : lucky
			// подумав над возможностью абстрагироваться от вездесущих echo...
			//
			// В этих методах можно абстрагировать вывод от функций Http::* и echo
			//
			// interface IHttpWriter
			// {
			//   function writeHeader(String $string);
			//   function writeData(String $string);
			// }
			//
			// тогда это паттерн Protocol-Transport (смотрим где-то в районе twisted-python).
			// хотя, наверняка, придется отказаться от passthru() ..
		} while (false);
		//$this->setHeader('Connection', 'close');
		$this->sendStatus($this->getStatusCode());
		$this->sendHeaders($this->getHeaders());
		if ($allow_send_content) $this->sendContent($content->getDataStream(), $from, $to);

		session_write_close();

		//while (@ob_end_clean()); 		// 61: Из за этой строки почему-то не отдавался контент по завершении скрипта.
				// Как следствие на некоторых конфигурациях LAMP не отдавались CSS, JS и картинки с жалобами на тормоза
		flush();
		return '';
	} // end of function send

	//!a manipulator
	public function setDownloadMode ($value)
	/**
	 * setDownloadMode установить режим отдачи контента.
	 *
	 * Можешь отдать контент, так чтобы он отображался в браузере (::MODE_DIRECT),
	 * а можешь попытаться заставить браузер показать диалог "Сохранить как..." (::MODE_ATTACHMENT)
	 *
	 * В конечном счете можешь забить (::MODE_AUTO) и дальше мы тут сами разбиремся.
	 *
	 * @param mixed $value режим отдачи контента: один из { ::MODE_ATTACHMENT, ::MODE_DIRECT, ::MODE_AUTO }
	 * @access public
	 * @return void
	 */
	{
		$this->download_mode = $value;
		return $this;
	} // end of function setMode

	//!an accessor
	public function getDownloadMode ()
	/**
	 * getDownloadMode вернуть режим отдачи контента.
	 *
	 * @access public
	 * @return void
	 */
	{
		return $this->download_mode;
	} // end of function getDownloadMode

	//!an accessor
	public function getHeader ($name)
	/**
	 * getHeader вернуть HTTP заголовок по его имени.
	 *
	 * @param mixed $name имя заголовка
	 * @access public
	 * @return string
	 */
	{
		return (isset($this->headers[$name])) ? $this->headers[$name]
		                                      : NULL;
	} // end of function getHeader

	//!a manipulator
	public function setHeader ($name, $value=NULL)
	/**
	 * setHeader установить HTTP заголовок.
	 *
	 * @param  mixed $name имя заголовка
	 * @param  mixed $value значение
	 * @access public
	 * @return Object $this
	 */
	{
		if (!isset($value)) {
			list($name, $value) = explode(':', $name, 2);
		}
		$this->headers[$name] = $value;
		return $this;
	} // end of function header

	//!an accessor
	public function getHeaders ()
	/**
	 * getHeaders вернуть хеш-список HTTP заголовков.
	 *
	 * Ключи хеша -- имена заголовков.
	 *
	 * @access public
	 * @return Array список заголовков
	 */
	{
		return $this->headers;
	} // end of function getHeaders

	//!a manipulator
	public function setStatusCode ($value)
	/**
	 * setStatusCode установить код HTTP статуса
	 *
	 * @param Integer $value код HTTP статуса
	 * @access public
	 * @return $this
	 */
	{
		$this->status_code = $value;
		return $this;
	} // end of function setStatusCode

	//!a function
	public function findStatusCode ($status)
	/**
	 * findStatusCode в зависимости от объекта-статуса вернуть соответствующий код HTTP статуса.
	 *
	 * @param RL_Request_Status $status объект-статус
	 * @access public
	 * @return Integer код HTTP статуса
	 */
	{
		return isset(self::$status_codes[$status]) ? self::$status_codes[$status] : '500' /* server error */;
	} // end of function findStatusCode

	//!an accessor
	public function getStatusCode ()
	/**
	 * getStatusCode вернуть HTTP статус.
	 *
	 * @access public
	 * @return Integer HTTP статус. код (200, 404, ..)
	 */
	{
		return $this->status_code;
	} // end of function getStatusCode

	//!a procedure
	public function sendStatus ($status)
	/**
	 * sendStatus отправить HTTP статус.
	 *
	 * @param mixed $status код (200, 404, и т.п.)
	 * @access public
	 * @return void
	 */
	{
		Http::status($status);
	} // end of function sendStatus

	//!a procedure
	public function sendHeader ($name, $value)
	/**
	 * sendHeader отправить HTTP заголовок
	 *
	 * @param mixed $name имя заголовка
	 * @param mixed $value значение
	 * @access public
	 * @return void
	 */
	{
		Http::header($name . ': ' . $value);
	} // end of function sendHeader

	//!a procedure
	public function sendHeaders ($headers)
	/**
	 * sendHeaders отправить HTTP заголовки.
	 *
	 * @param Array $headers список заголовков array( 'Header-name' => value, ... )
	 * @access public
	 * @return void
	 */
	{
		foreach ($headers as $name=>$value) {
			$this->sendHeader($name, $value);
		}
	} // end of function sendHeaders

	//!an accessor
	public function sendContent ($stream, $from=NULL, $to=NULL)
	/**
	 * sendContent отправить контент.
	 *
	 * @param RL_Stream $stream поток, из которого читать данные
	 * @param mixed $from начальная позиция в потоке, с которой отправлять данные
	 * @param mixed $to конечная позиция в потоке, до которой отправлять данные
	 * @access public
	 * @return void
	 */
	{
		// открыть поток на чтение
		$stream->open('rb');

		// установить начальную позицию
		if (isset($from)) {
			$stream->seek($from, SEEK_SET);
		}

		// если указана позиция, до которой выводить данные
		if (isset($to)) {
			$block_size = $this->bufsize;
			$to_send = $to;

			while (!$stream->eof() && $to_send > 0) {
				echo $stream->read(min($block_size, $to_send));
				$to_send -= $block_size;
			}
		} else {
		// для вывода до конца можно использовать (более быстрый метод?)
			$stream->passthru();
		}

		$stream->close();
	} // end of function sendContent

}

